/**
* Copyright (C) 2010-2017 Gordon Fraser, Andrea Arcuri and EvoSuite
* contributors
*
* This file is part of EvoSuite.
*
* EvoSuite is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3.0 of the License, or
* (at your option) any later version.
*
* EvoSuite is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
*/
package org.evosuite.maven;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.*;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.eclipse.aether.RepositorySystemSession;
import org.evosuite.Properties;
import org.evosuite.maven.util.EvoSuiteRunner;
import org.evosuite.maven.util.FileUtils;
import org.evosuite.maven.util.HistoryChanges;
import org.evosuite.utils.SpawnProcessKeepAliveChecker;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Generate JUnit tests
*/
@Mojo( name = "generate" , requiresDependencyResolution = ResolutionScope.TEST, requiresDependencyCollection = ResolutionScope.TEST)
@Execute(phase = LifecyclePhase.COMPILE)
public class GenerateMojo extends AbstractMojo {
/**
* Total Memory (in MB) that CTG will use
*/
@Parameter( property = "memoryInMB", defaultValue = "800" )
private int memoryInMB;
/**
* Number of cores CTG will use
*/
@Parameter( property = "cores", defaultValue = "1" )
private int numberOfCores;
/**
* Comma ',' separated list of CUTs to use in CTG. If none specified, then test all classes
*/
@Parameter( property = "cuts" )
private String cuts;
/**
* Absolute path to a file having the list of CUTs specified. This is needed for operating
* systems like Windows that have constraints on the size of input parameters and so could
* not use "cuts" parameter instead if too many CUTs are specified
*/
@Parameter( property = "cutsFile")
private String cutsFile;
/**
* How many minutes to allocate for each class
*/
@Parameter( property = "timeInMinutesPerClass", defaultValue = "2" )
private int timeInMinutesPerClass;
/**
* How many minutes to allocate for each project/module. If this parameter is not set, then the total time will be timeInMinutesPerClass x number_of_classes
*/
@Parameter( property = "timeInMinutesPerProject", defaultValue = "0" )
private int timeInMinutesPerProject;
/**
* Coverage criterion. Can define more than one criterion by using a ':' separated list
*/
// FIXME would be nice to have the value of Properties.CRITERION but seems to be not possible
@Parameter( property = "criterion", defaultValue = "LINE:BRANCH:EXCEPTION:WEAKMUTATION:OUTPUT:METHOD:METHODNOEXCEPTION:CBRANCH" )
private String criterion;
@Parameter(property = "spawnManagerPort", defaultValue = "")
private Integer spawnManagerPort;
@Parameter( property = "extraArgs" , defaultValue = "")
private String extraArgs;
/**
* Schedule used to run CTG (SIMPLE, BUDGET, SEEDING, BUDGET_AND_SEEDING, HISTORY)
*/
@Parameter( property = "schedule", defaultValue = "BUDGET" )
private String schedule;
@Parameter(defaultValue = "${project}", required = true, readonly = true)
private MavenProject project;
@Parameter(defaultValue = "${plugin.artifacts}", required = true, readonly = true)
private List<Artifact> artifacts;
@Component
private ProjectBuilder projectBuilder;
@Parameter(defaultValue="${repositorySystemSession}", required = true, readonly = true)
private RepositorySystemSession repoSession;
/**
* Defines files in the source directories to include (all .java files by default).
*/
private String[] includes = {"**/*.java"};
/**
* Defines which of the included files in the source directories to exclude (non by default).
*/
private String[] excludes;
@Override
public void execute() throws MojoExecutionException, MojoFailureException{
getLog().info("Going to generate tests with EvoSuite");
getLog().info("Total memory: "+memoryInMB+"mb");
getLog().info("Time per class: "+timeInMinutesPerClass+" minutes");
getLog().info("Number of used cores: "+numberOfCores);
if(cuts!=null){
getLog().info("Specified classes under test: "+cuts);
}
String target = null;
String cp = null;
try {
//the targets we want to generate tests for, ie the CUTs
for(String element : project.getCompileClasspathElements()){
if(element.endsWith(".jar")){ // we only target what has been compiled to a folder
continue;
}
File file = new File(element);
if(!file.exists()){
/*
* don't add to target an element that does not exist
*/
continue;
}
if(! file.getAbsolutePath().startsWith(project.getBasedir().getAbsolutePath()) ){
/*
This can happen in multi-module projects when module A has dependency on
module B. Then, both A and B source folders will end up on compile classpath,
although we are interested only in A
*/
continue;
}
if(target == null){
target = element;
} else {
target = target + File.pathSeparator + element;
}
}
//build the classpath
Set<String> alreadyAdded = new HashSet<>();
for(String element : project.getTestClasspathElements()){
if(element.toLowerCase().contains("powermock")){
//PowerMock just leads to a lot of troubles, as it includes tools.jar code
getLog().warn("Skipping PowerMock dependency at: "+element);
continue;
}
if(element.toLowerCase().contains("jmockit")){
//JMockit has same issue
getLog().warn("Skipping JMockit dependency at: "+element);
continue;
}
getLog().debug("TEST ELEMENT: "+element);
cp = addPathIfExists(cp, element, alreadyAdded);
}
} catch (DependencyResolutionRequiredException e) {
getLog().error("Error: "+e.getMessage(),e);
return;
}
File basedir = project.getBasedir();
getLog().info("Target: "+target);
getLog().debug("Classpath: "+cp);
getLog().info("Basedir: "+basedir.getAbsolutePath());
if(target==null || cp==null || basedir==null){
getLog().info("Nothing to test");
return;
}
runEvoSuiteOnSeparatedProcess(target, cp, basedir.getAbsolutePath());
}
private String addPathIfExists(String cp, String element, Set<String> alreadyExist) {
File file = new File(element);
if(!file.exists()){
/*
* don't add to CP an element that does not exist
*/
return cp;
}
if(alreadyExist.contains(element)){
return cp;
}
alreadyExist.add(element);
if(cp == null){
cp = element;
} else {
cp = cp + File.pathSeparator + element;
}
return cp;
}
private void runEvoSuiteOnSeparatedProcess(String target, String cp, String dir) throws MojoFailureException {
List<String> params = new ArrayList<>();
params.add("-continuous");
params.add("execute");
params.add("-target");
params.add(target);
params.add("-Dcriterion=" + criterion);
params.add("-Dctg_schedule=" + schedule);
if (schedule.toUpperCase().equals(Properties.AvailableSchedule.HISTORY.toString())) {
try {
List<File> files = FileUtils.scan(this.project.getCompileSourceRoots(), this.includes, this.excludes);
HistoryChanges.keepTrack(dir, files);
} catch (Exception e) {
throw new MojoFailureException("", e);
}
params.add("-Dctg_history_file=" + dir + File.separator + Properties.CTG_DIR + File.separator + "history_file");
}
params.add("-Dctg_memory="+memoryInMB);
params.add("-Dctg_cores="+numberOfCores);
int port;
if(spawnManagerPort != null) {
SpawnProcessKeepAliveChecker.getInstance().registerToRemoteServerAndDieIfFails(spawnManagerPort);
port = spawnManagerPort;
} else {
port = SpawnProcessKeepAliveChecker.getInstance().startServer();
}
params.add("-Dspawn_process_manager_port=" + port);
if (timeInMinutesPerProject != 0) {
params.add("-Dctg_time="+timeInMinutesPerProject);
params.add("-Dctg_min_time_per_job="+timeInMinutesPerClass);
} else {
params.add("-Dctg_time_per_class="+timeInMinutesPerClass); // there is no time limit, so test all classes X minutes
}
if(cuts!=null){
params.add("-Dctg_selected_cuts="+cuts);
}
if(cutsFile!=null){
params.add("-Dctg_selected_cuts_file_location="+cutsFile);
}
if(extraArgs!=null && !extraArgs.isEmpty()){
String args = "";
//note this does not for properly for parameters with strings using spaces
String[] tokens = extraArgs.split(" ");
for(String token : tokens){
token = token.trim();
if(token.isEmpty()){
continue;
}
if(!token.startsWith("-D")){
getLog().error("Invalid extra argument \""+token+"\". It should start with a -D");
} else {
args += " " +token;
}
}
params.add("-Dctg_extra_args=\""+args+"\"");
}
String path = writeClasspathToFile(cp);
params.add("-DCP_file_path="+path);
//params.add("-DCP=" + cp); //this did not work properly on Windows
EvoSuiteRunner runner = new EvoSuiteRunner(getLog(),artifacts,projectBuilder,repoSession);
runner.registerShutDownHook();
boolean ok = runner.runEvoSuite(dir,params);
if(spawnManagerPort != null) {
SpawnProcessKeepAliveChecker.getInstance().unRegister();
} else {
SpawnProcessKeepAliveChecker.getInstance().stopServer();
}
if(!ok){
throw new MojoFailureException("Failed to correctly execute EvoSuite");
}
}
private String writeClasspathToFile(String classpath) {
try {
File file = File.createTempFile("EvoSuite_classpathFile",".txt");
file.deleteOnExit();
BufferedWriter out = new BufferedWriter(new FileWriter(file));
String line = classpath;
out.write(line);
out.newLine();
out.close();
return file.getAbsolutePath();
} catch (Exception e) {
throw new IllegalStateException("Failed to create tmp file for classpath specification: "+e.getMessage());
}
}
}